package il.ac.tau.arielgue.outxing.contexts;

import il.ac.tau.arielgue.outxing.agents.OutXingOrganism;
import il.ac.tau.arielgue.outxing.utils.Params;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;

import org.apache.commons.collections.MultiMap;
import org.jmock.core.constraint.IsInstanceOf;

import proevolution.AbstractOrganismContext;
import proevolution.agents.Organism;
import repast.simphony.engine.schedule.ScheduleParameters;
import repast.simphony.engine.schedule.ScheduledMethod;
import repast.simphony.random.RandomHelper;

import com.google.common.collect.Collections2;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;

/**
 * This abstract class has methods and members that are common to
 * {@link DemeSubContext} and {@link PoolSubContext}, such as
 * createNewGeneration, selectParents etc.
 * 
 * @author arielgue TODO move some of the methods to AbstractSexualContext
 */
public abstract class AbstractOutXingSubContext extends AbstractOrganismContext {

	private static final long serialVersionUID = 5541294504542351638L;

	private OutXingContext masterContext = null;

	private int demePopulationCapacity = -1;

	public AbstractOutXingSubContext() {
		super();
	}

	public AbstractOutXingSubContext(Object name, Object typeID) {
		super(name, typeID);
	}

	public AbstractOutXingSubContext(Object name) {
		super(name);
	}

	/**
	 * creates a new generation from the current generation through:
	 * <ul>
	 * <li>parental selection - select an even number of parents for new
	 * generation</li>
	 * <li>mating - mate selected parents</li>
	 * <li>reproduce - reproduce mated parents</li>
	 * <li>"kill" all old generation and add new generation (children) to the
	 * context</li>
	 * </ul>
	 * called by schedule
	 * 
	 * </br>TODO change OutXingOrganism to SexualOrganism
	 */
	public void createNewGeneration() {
		/*
		 * select parents. map key is male, value is the collection of females
		 * to mate with the male.
		 */
		Multimap<OutXingOrganism, OutXingOrganism> parents = selectParents();

		Collection<Organism> children = new HashSet<Organism>();

		// reproduce
		for (OutXingOrganism male : parents.keySet()) {
			for (OutXingOrganism female : parents.get(male)) {
				int numOfChildren = calculateNumberOfChildren(male, female);
				for (int j = 0; j < numOfChildren; j++) {
					// ** Juvenile Selection **
					Organism child = male.reproduce(female);
					if (doSelectionOnOrganism(child)) {
						children.add(child);
					}
				}
			}
		}

		// kill old generation
		for (Organism o : getOrganisms()) {
			o.die();
			remove(o);
		}

		// add new generation
		this.addAll(children);
	}

	/**
	 * kills random organisms according to the cost of predation. the effect of
	 * predators can be overcome only when conditions are good (but not too good
	 * - i.e. 0.8<size<1 ), else it is another cost. called by master context.
	 */
	public void predatorsEffect() {
		double predationEffectSize = size()
				* (1 - Params.getPredationEffectFraction());
		while ((size() < getDemePopulationCapacity() * 0.8 || size() > getDemePopulationCapacity())
				&& size() > predationEffectSize) {
			Organism o = getRandomOrganism();
			o.die();
			remove(o);
		}
	}

	/**
	 * kills random organisms and also makes sure that the population is not
	 * larger than the maximum population size. called by master context.
	 */
	public void randomDrift() {
		// if (this instanceof DemeSubContext) {
		while (size() > getDemePopulationCapacity()) { // instead of
			// getMaxPopulationSize. getDemePopulationCapacity() includes
			// climate/environmental effect
			Organism o = getRandomOrganism();
			o.die();
			remove(o);
		}

		int a = 0; // debug print
	}

	/**
	 * Creates a multimap of organisms, key is a male and value is the females
	 * to mate with the male. All females appear on the map exactly once, but
	 * males appear 0 or 1. Each male can have multiple partners (thus a
	 * {@link Multimap}. Mating is done on random (uniformly).
	 * 
	 * TODO change OutXingOrganism to SexualOrganism
	 * 
	 * @return a {@link MultiMap} of organisms, key is male, value is collection
	 *         of females to mate with male.
	 */
	protected Multimap<OutXingOrganism, OutXingOrganism> selectParents() {
		// key is male, value is collection of females to mate with male
		Multimap<OutXingOrganism, OutXingOrganism> parents = HashMultimap
				.create();
		Collection<Organism> organisms = getOrganisms();
		if (organisms.size() == 0)
			return parents;

		List<OutXingOrganism> males = new ArrayList<OutXingOrganism>();
		List<OutXingOrganism> females = new ArrayList<OutXingOrganism>();

		// sort males and females.
		for (Organism o : organisms) {
			if (o instanceof OutXingOrganism) {
				OutXingOrganism oo = (OutXingOrganism) o;
				if (1 == oo.isMale()) {
					males.add(oo);
				} else {
					females.add(oo);
				}
			} else {
				throw new ClassCastException("Organism " + o.getID()
						+ " is of type " + o.getClass().getName()
						+ ", expected " + OutXingOrganism.class.getName());
			}
		}

		if (males.size() == 0 || females.size() == 0)
			return parents;

		for (OutXingOrganism female : females) {
			// choose random male
			int maleIndex = RandomHelper.nextIntFromTo(0, males.size() - 1);
			// mate this female with randomly chosen male
			parents.put(males.get(maleIndex), female);
		}
		return parents;
	}

	protected int calculateNumberOfChildren(Organism male, Organism female) {
		return Params.getNumberOfChildren();
	}

	
	private int getDemeCarryingCapacity() {
		return Params.getDemeCarryingCapacity();
	}
	// getDemePopulationCapacity() includes climate/environmental effect through
	// changeWeather().
	public int getDemePopulationCapacity() {
		if (this.demePopulationCapacity < 0) {
			return getDemeCarryingCapacity();
		} else
			return this.demePopulationCapacity;
	}

	public void setDemePopulationCapacity(int size) {
		this.demePopulationCapacity = size;
	}

	public double getMeanFitness() {
		double sum = 0;
		for (Organism o : this) {
			sum += o.getFitness();
		}
		return sum / size();
	}

	public double getMeanHeterozygosity() {
		double sum = 0;
		for (Organism o : this) {
			sum += ((OutXingOrganism) o).getHeterozigosity();
		}
		return sum / size();
	}

	public double getMeanDeleteriousHomozigosity() {
		double sum = 0;
		for (Organism o : this) {
			sum += ((OutXingOrganism) o).getDeleteriousHomozigosity();
		}
		return sum / size();
	}

	/**
	 * 
	 * @param organism
	 * @return true is the organism survives selection
	 */
	protected boolean doSelectionOnOrganism(Organism organism) {
		return RandomHelper.nextDouble() < organism.getFitness();
	}

	public void setMasterContext(OutXingContext masterContext) {
		this.masterContext = masterContext;
	}

	public OutXingContext getMasterContext() {
		return masterContext;
	}

	/**
	 * avoid serialization of subcontext
	 */
	@Override
	public String serialize() {
		return null;
	}

}
